﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using DotRas;
using System.Diagnostics;
using System.IO;
using System.Text.RegularExpressions;
using System.Net;

namespace TrafficSplitter
{
    class VirtualInterface
    {
        private String GatewayIPAddress;
        private TS_STATES State;
        private VirtualAdapter Adapter;
        private RasConnectionWatcher Watcher;
        private RasDialer Dialer;
        private TS_INTERFACES Interface;
        private String Filename;
        private List<String> Routes;
        private AdapterFunctionsRAS InterfacesRAS;
        private AdapterFunctionsWMI InterfacesAdapter;
        private AdapterFunctionsInterface GenericInterface;
        private Boolean continueSetupRoutes;
        private String identifier;
        public event EventHandler adapterstatechanged;
        public event EventHandler errorOnConnect;

        public VirtualInterface(String filename, TS_INTERFACES interface_type, AdapterFunctionsRAS rasInterfaces, AdapterFunctionsWMI adapterInterfaces)
        {
            this.Filename = filename;
            this.Interface = interface_type;
            this.InterfacesAdapter = adapterInterfaces;
            this.InterfacesRAS = rasInterfaces;
            this.Routes = new List<String>();
            this.ImportRoutes();
            this.adapterstatechanged += new EventHandler(adapterStateChanged);
        }

        public void UpdateAdapter(VirtualAdapter adapter)
        {
            this.Adapter = adapter;
            this.identifier = this.Interface.ToString() + " [" + this.Adapter.GetEntryName() + "]";

            if (adapter.GetDeviceType() == TS_ADAPTER_TYPES.RASDevice)
            {
                this.GenericInterface = this.InterfacesRAS;

                // Set Dialer info
                this.Dialer = new RasDialer();
                //this.Dialer.EapData = null;
                this.Dialer.Timeout = Constants.DIALER_TIMEOUT;
                this.Dialer.PhoneBookPath = RasPhoneBook.GetPhoneBookPath((adapter.GetDeviceID() == 0) ? RasPhoneBookType.User : RasPhoneBookType.AllUsers);
                this.Dialer.EntryName = adapter.GetEntryName();
                this.Dialer.DialCompleted += new EventHandler<DialCompletedEventArgs>(dialCompleted);
                this.Dialer.StateChanged += new EventHandler<StateChangedEventArgs>(stateChanged);

                // Update watcher
                this.Watcher = new RasConnectionWatcher();
                this.Watcher.Disconnected += new EventHandler<RasConnectionEventArgs>(watcherDisconnected);
                this.Watcher.Connected += new EventHandler<RasConnectionEventArgs>(watcherConnected);
                this.Watcher.EnableRaisingEvents = false;
            }
            else if (adapter.GetDeviceType() == TS_ADAPTER_TYPES.NetworkAdapter)
            {
                this.GenericInterface = this.InterfacesAdapter;

                this.Dialer = null;
                this.Watcher = null;
            }
            else
            {
                this.GenericInterface = new AdapterFunctionsNone();

                this.Dialer = null;
                this.Watcher = null;
            }

            RefreshState();
        }

        public void RefreshState()
        {
            TS_STATES state = this.GenericInterface.GetInterfaceState(this.Adapter);

            // Get default gateway IP address
            this.GatewayIPAddress = this.GenericInterface.GetGatewayIPAddress(this.Adapter);

            // Get state
            this.SetState(state);
        }

        private void SetState(TS_STATES state)
        {
            if ((this.State != state))
            {
                if (state != TS_STATES.Unknown)
            {
                 this.State = state;
                adapterstatechanged(this, null);
            }
            }
        }

        private void AppendToLog(LOG_TYPE type, String str)
        {
            Constants.AppendToLog(type, this.identifier + ": " + str);
        }

        private void AppendToLogBuffer(LOG_TYPE type, String str)
        {
            Constants.AppendToLogBuffer(type, this.identifier + ": " + str);
        }

        private void stateChanged(object sender, StateChangedEventArgs e)
        {
            AppendToLog(LOG_TYPE.Debug, " state changed to: " + e.State.ToString());
        }

        public void Dial()
        {
            if (this.Dialer != null)
            {
                RefreshState();
                if (this.State == TS_STATES.Disconnected)
                {
                    try
                    {
                        NetworkCredential storedCredentials = RasHelper.GetCredentials(
                                this.Dialer.PhoneBookPath,
                                this.Dialer.EntryName,
                                RasHelper.RASCM.UserName | RasHelper.RASCM.Password | RasHelper.RASCM.Domain);
                        if ((storedCredentials == null) || (string.IsNullOrEmpty(storedCredentials.Password)))
                        {
                            string msg = string.Format("Please save the username and password before attempting to connect {0}", this.Dialer.EntryName);
                            MessageBox.Show(msg, Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Warning);
                            return;
                        }

                        Application.DoEvents();
                        this.SetState(TS_STATES.Dialing);
                        this.Dialer.AllowUseStoredCredentials = true;
                        this.Dialer.DialAsync();
                    }
                    catch (RasDialException e)
                    {
                        Constants.AppendToLog(LOG_TYPE.Error, e.ToString());
                    }
                    catch (RasException e)
                    {
                        if (e.ErrorCode == 756)
                        {
                            // Connection already dialed
                            this.State = TS_STATES.Dialing;
                        }
                        else
                        {
                            Constants.AppendToLog(LOG_TYPE.Error, e.ToString());
                        }
                    }
                    catch (Exception e)
                    {
                        Constants.AppendToLog(LOG_TYPE.Error, e.ToString());
                    }
                }
            }
        }

        public void DialCancel()
        {
            if (this.Dialer != null)
            {
                try
                {
                    Application.DoEvents();
                    this.SetState(TS_STATES.Disconnecting);
                    this.Dialer.DialAsyncCancel();
                }
                catch (RasDialException e)
                {
                    Constants.AppendToLog(LOG_TYPE.Error, e.ToString());
                }
                catch (Exception e)
                {
                    Constants.AppendToLog(LOG_TYPE.Error, e.ToString());
                }
            }
        }

        public void Disconnect()
        {
            RasConnection conn = this.InterfacesRAS.FindThisActiveConnection(this.Adapter);
            if (conn != null)
            {
                Application.DoEvents();
                this.SetState(TS_STATES.Disconnecting);
                try
                {
                    conn.HangUp();
                }
                catch (Exception e)
                {
                    Constants.AppendToLog(LOG_TYPE.Error, e.ToString());
                }
            }
            else
            {
                RefreshState();
            }
        }

        public VirtualAdapter GetAdapter()
        {
            return this.Adapter;
        }

        public TS_STATES GetState()
        {
            return this.State;
        }

        public TS_INTERFACES GetInterfaceType()
        {
            return this.Interface;
        }

        public Int32 GetTotalRoutes()
        {
            return this.Routes.Count;
        }

        public List<String> GetRoutes()
        {
            return this.Routes;
        }

        public void AbortRouteAddition()
        {
            this.continueSetupRoutes = false;
        }


        public void SetupRoutes(Boolean add, Boolean persistent)
        {
            this.continueSetupRoutes = true;

            if (this.Adapter.GetDeviceType() != TS_ADAPTER_TYPES.None)
            {
                this.RefreshState();

                if ((this.State != TS_STATES.Connected) && (this.State != TS_STATES.Unknown))
                {
                    String msg = this.identifier + " interface not connected";
                    Constants.AppendToLog(LOG_TYPE.Error, msg);
                    throw new Exception(msg);
                }
                else if (this.GatewayIPAddress == null)
                {
                    String msg = "Unknown gateway IP address for " + this.identifier;
                    Constants.AppendToLog(LOG_TYPE.Error, msg);
                    throw new Exception(msg);
                }
                else
                {
                    Boolean succ = true;
                    Int32 count = 0;
                    String args;
                    UInt32 index = this.Adapter.GetIndex();
                    int i = 0;
                    while ((index == 0) && (i < Constants.DIALER_TIMEOUT))
                    {
                        i++;
                        this.RefreshState();
                        Application.DoEvents();
                        System.Threading.Thread.Sleep(1000);
                        index = this.Adapter.GetIndex();
                    }
                    UInt32 metric = this.Adapter.GetMetric();

                    foreach (String ipAndMask in this.Routes)
                    {
                        count++;

                        // Get destination IP address & mask
                        String[] ret = ipAndMask.Split(',');
                        if (add)
                        {
                            if (GatewayIPAddress != null)
                                args = (persistent ? "-p add " : "add ") + ret[0] + " mask " + ret[1] + " " + this.GatewayIPAddress + " if " + index.ToString() + " metric " + metric;
                            else
                                args = (persistent ? "-p add " : "add ") + ret[0] + " mask " + ret[1] + " if " + index.ToString() + " metric " + metric;
                        }
                        else
                            args = "delete " + ret[0] + " mask " + ret[1] + " if " + index.ToString(); ;

                        // Set up route
                        //Boolean route = this.RunCommand("route", args);
                        Boolean route = Routing.Modify(add, ret[0], ret[1], this.GatewayIPAddress, index, metric);

                        if (route)
                            AppendToLogBuffer(LOG_TYPE.Info, "route " + args);
                        else
                        {
                            AppendToLogBuffer(LOG_TYPE.Error, "route " + args);
                            succ = false;
                        }

                        if (!continueSetupRoutes)
                            break;
                    }

                    Constants.FlushLogBuffer();
                    if (!succ)
                    {
                        if (add)
                            throw new Exception("Error(s) occured during addition of " + this.identifier + " route entries");
                        else
                            throw new Exception("Error(s) occured during removal of " + this.identifier + " route entries");
                    }
                }
            }
        }

        private void adapterStateChanged(object sender, EventArgs e)
        {
            if ((this.Adapter.GetDeviceType() == TS_ADAPTER_TYPES.RASDevice) && (this.Watcher.Handle == null))
            {
                RasHandle handle = this.Adapter.GetHandle();
                if (handle == null)
                {
                    RasConnection conn = this.InterfacesRAS.FindThisActiveConnection(this.Adapter);
                    if (conn != null)
                    {
                        this.Watcher.Handle = conn.Handle;
                        this.Adapter.SetHandle(conn.Handle);
                        this.Watcher.EnableRaisingEvents = true;
                    }
                }
                else
                {
                    this.Watcher.Handle = handle;
                    this.Watcher.EnableRaisingEvents = true;
                }
            }

            if (this.State == TS_STATES.Connected)
                AppendToLog(LOG_TYPE.Info, "is now connected.");
            else if (this.State == TS_STATES.Disconnected)
                AppendToLog(LOG_TYPE.Info, "is now disconnected.");
        }

        private void dialCompleted(Object sender, DialCompletedEventArgs e)
        {
            if (!e.Connected)
            {
                errorOnConnect(this, null);
                string msg = string.Format("Unable to connect {0}\n\nError message:\n{1}", this.Adapter.GetEntryName(), e.Error.Message);
                MessageBox.Show(msg, Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Error);
            }

            RefreshState();
        }

        private void watcherConnected(object sender, RasConnectionEventArgs e)
        {
            this.SetState(TS_STATES.Connected);
        }

        private void watcherDisconnected(object sender, RasConnectionEventArgs e)
        {
            this.SetState(TS_STATES.Disconnected);
        }

        public void ImportRoutes()
        {
            StreamReader sr;
            String str = "";
            Match ret;
            this.Routes.Clear();

            try
            {
                sr = File.OpenText(Application.StartupPath + "\\" + this.Filename);

                while (str != null)
                {
                    str = sr.ReadLine();
                    if (str != null)
                    {
                        // Filter IP address & subnet mask
                        ret = Constants.ipAndMaskPattern.Match(str);
                        if (ret.Success)
                            this.Routes.Add(ret.Value);
                    }
                }
                sr.Close();

                AppendToLog(LOG_TYPE.Debug, "Successfully imported routes from " + this.Filename);
            }
            catch (System.IO.FileNotFoundException e)
            {
                Constants.AppendToLog(LOG_TYPE.Error, e.Message);
            }
            catch (System.Exception e)
            {
                Constants.AppendToLog(LOG_TYPE.Error, e.Message);
            }
        }

        private Boolean RunCommand(String command, String arguments)
        {
            Application.DoEvents();
            Process p = new Process();
            p.StartInfo.UseShellExecute = false;
            p.StartInfo.FileName = command;
            p.StartInfo.Arguments = arguments;
            p.StartInfo.RedirectStandardError = true;
            p.StartInfo.StandardErrorEncoding = Encoding.ASCII;
            p.StartInfo.RedirectStandardOutput = true;
            p.StartInfo.StandardOutputEncoding = Encoding.ASCII;
            p.StartInfo.CreateNoWindow = true;
            p.Start();

            String output = p.StandardError.ReadToEnd();
            if ((output != "") && !output.Contains("OK!"))
            {
                AppendToLog(LOG_TYPE.Error, "Error with '\"" + command + "\" " + arguments + "'");
                AppendToLog(LOG_TYPE.Error, output.Trim());
                return false;
            }

            return true;
        }
    }
}
